# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
__docformat__ = "restructuredtext en"

from collections import Counter
import sys
import os
import logging

Logger = logging.getLogger('PySolFC')
HANDLER = logging.StreamHandler()
HANDLER.setFormatter(logging.Formatter('%(levelname)s %(module)s '
                                        '%(funcName)s %(lineno)d '
                                        '%(message)s'))
Logger.addHandler(HANDLER)


from .viewlib.dialog import Dialog
from .viewlib.constants import CURSOR_WATCH
import settings
from resourceng import TILE_DB
from persistance import NOTE_DB
from .settings import CONF_OPT, TITLE, VERSION
from .pysolaudio import AUDIO_DEV
from .utillib.pathdb import PATH_DB

LEVELS = {'debug': logging.DEBUG, 'info': logging.INFO,
          'warning': logging.WARNING, 'error': logging.ERROR,
          'critical': logging.CRITICAL}
DEBUG, CHECK = 'PYSOL_DEBUG', 'PYSOL_CHECK_GAMES'


def parse_optionargs():
    """
    parse_optionargs() -> (object, str)

    Parse the passed command for appended options and return as an object.
    Also returned is the search of remaing args for a saved game filename.

      - object.gn: a valid pysol game name
      - object.gi: a valid pysol game id
      - object.gs: main pysol game style types
      - object.sound: a valid pysol sound server
      - object.nosound
      - object.noplugins
      - object.nogamesmenu
      - object.gs_all: selection of any gs options
    """
    from optparse import OptionParser
    usage = 'usage: %prog [options] [FILE]'
    parser = OptionParser(usage=usage)
    parser.add_option('--game', dest='gn', default=None,
                      help='start game GAME NAME')
    parser.add_option('--gameid', dest='gi', type='int', default=None,
                      help='start game GAME ID')
    parser.add_option('--french', '-f', action='append_const', dest='gs',
                      const=1, help='Show only French-style games')
    parser.add_option('--tarock', '-t', action='append_const', dest='gs',
                      const=2, help='Show only Tarock-style games')
    parser.add_option('--dashavatara', '-d', action='append_const', dest='gs',
                      const=4, help='Show only Dashavatara-style games')
    parser.add_option('--mughal', '-m', action='append_const', dest='gs',
                      const=8, help='Show only Mughal-style games')
    parser.add_option('--hanafuda', '-n', action='append_const', dest='gs',
                      const=16, help='Show only Hanafuda-style games')
    parser.add_option('--hexadeck', '-x', action='append_const', dest='gs',
                      const=32, help='Show only Hex-A-Deck-style games')
    parser.add_option('--mahjong', '-j', action='append_const', dest='gs',
                      const=64, help='Show only Mahjong-style games')
    parser.add_option('--shisensho', '-s', action='append_const', dest='gs',
                      const=256, help='Show only Shisen-Sho-style games')
    parser.add_option('--sound-mod', dest='sound', default='pygame',
                      choices=('pygame', 'win'),  # 'oss', 'pss'),
                      help='one of following: pss, pygame(default), oss, win')
    parser.add_option('--nosound', dest='nosound', action='store_true')
    parser.add_option('--noplugins', dest='noplugins', action='store_true')
    parser.add_option('--nogamesmenu', dest='nogamesmenu', action='store_true')
    return parser.parse_args()


class PysolApp(object):
    """
    PysolApp context manager that sets intial state for the various parts.
    """
    canvas = None                       # Canvas
    _canvas = None                      # ScrolledCanvas
    nextgame = {'id': 0,                # start this game
                'random': None,         # use this random generator
                'data': None,           # data for loaded game
                'bookmark': {},       #
                'mode': None,           # start mode ?
                'counter': Counter(),
                }
    demo_counter = 0
    progress = None

    def __enter__(self):
        """
        __enter__() -> self

        Initialize app for use with PysolApp.mainloopt().

        :IVariables:
          progress : dialog.progress
            provide feedback during initialization

        :returns: initialized PysolApp object
        """
        self._init_debug()
        PATH_DB.init(sys.argv[0])
        self._load_config()
        args, filename = self._read_cmdarg()
        self._init_window(args)
        self.progress = progress = Dialog.progress()
        # Load GAME_DB
        from .gamelib.data import prepare
        gid = prepare(args, progress)
        if gid is None:
            self.fatal_error(text=_('No games found.'), err=1)
        else:
            self.nextgame['id'] = gid
        self._init_sounddb(args)
        self._init_carddb(args)
        self._init_tiledb(args)
        from .statdb import STAT_DB
        STAT_DB.load(PATH_DB.fn['stats'])
        NOTE_DB.load(PATH_DB.fn['notes'])
        self.splashscreen = True
        self._load_held_game()
        self._load_save_game(load=filename)
        CONF_OPT['game_holded'] = False
        self._load_canvas()
        return self

    def _init_debug(self):
        """
        init_debug() -> None

        Set debug level of Logger and check games based on environment values.
        """
        if DEBUG in os.environ and os.environ[DEBUG] in LEVELS:
            logging.basicConfig(level=LEVELS[os.environ[DEBUG]])
            Logger.info('debug level: set to %s.', os.environ[DEBUG])
        else:
            logging.getLogger('PySolFC').addHandler(logging.NullHandler())
        if  (CHECK in os.environ and type(os.environ[CHECK]) is bool):
            settings.CHECK_GAMES = True
            Logger.info('Set CHECK_GAMES to True')

    def _load_config(self):
        """
        _load_config() -> None

        Load and verify config file and set player to user name.

        :IVariables:
          opt : CONF_OPT
            Options from config file.

        :requires:
          ConfigObj

          Validator
        """
        import validate
        CONF_OPT.add_spec(infile=PATH_DB.fn['opt_conf'],
                          configspec=PATH_DB.fn['opt_spec'])

        validator = validate.Validator()
        if not CONF_OPT.validate(validator):
            Logger.warning('Invalid options file was found.')
            sys.exit(1)

        # Set player name to current user name
        try:
            player = [os.environ.get(n).strip()
                      for n in ('LOGNAME', 'USERNAME', 'USER',)
                          if n in os.environ][-1]
        except IndexError:
            player = 'Unknown User'
        CONF_OPT['player'] = player[:30]

    def _read_cmdarg(self):
        """
        _read_cmdarg() -> (object, file,)

        Read command line for passed args. Sets unparsed arg as loadgame. Sets
        settings.SELECT_GAME_MENU based on arg for nogamesmenu. Calls
        _loadArgsGame() to set any value from args.gn or args.gi
        """
        args, remains = parse_optionargs()
        if not args:
            sys.exit(1)

        args.gs = args.gs if args.gs else []
        args.gs_all = not sum(args.gs)

        settings.SELECT_GAME_MENU = bool(args.nogamesmenu)

        filename = None
        if len(remains) > 1:
            Logger.warning('More than one file was passed.')
        filename = remains and remains[0] or None
        if filename and not os.path.isfile(filename):
            Logger.warning('Invalid file name was passed.')
            filename = None
        return args, filename

    def _init_window(self, args):
        """
        _init_window(args) -> None

        Initialize window toolkit, setting root window and modal dialogs.

        :Parameters:
          args : object
            Arguments read from the command line

        :IVariables:
          top : MfxRoot
            main window.

          top_bg : object
            window background used by tile.tkwidget.ScrolledCanvas

          top_cursor : object
            window cursor

          dialog : Dialog
            basic modal dialogs.

        :requires:
          MfxRoot

          winsystems

          Dialog
        """
        from .viewlib.toolkit import Toplevel
        self.top = top = Toplevel(className=settings.TITLE)
        top.wm_title(TITLE + ' ' + VERSION)
        top.wm_iconname(TITLE + ' ' + VERSION)
        # set minsize
        if top.winfo_screenwidth() < 640 or top.winfo_screenheight() < 480:
            top.wm_minsize(400, 300)
        else:
            top.wm_minsize(520, 360)
        Dialog.connect_root(top)

    def _init_sounddb(self, args):
        """
        Initialize audio and load independent sound modules.

        :requires:
          pysolaudio

          SOUND_DB
        """
        from . import resourceng as r
        AUDIO_DEV.init(args)

        # Find and load available sound by file extension
        r.SOUND_DB.load(self, dirs=('sound', os.path.join('sound', 'extra')),
                      env=None, ext=AUDIO_DEV.formats, src=r.Sample)
        if self.progress:
            self.progress.update(step=1)

    def _init_carddb(self, args):
        """
        Initialize card modules and set next game cardset to default option.

        :requires:
          CARD_DB
        """
        from .packlib.data import CARD_DB
        CARD_DB._depth = self.top.winfo_screendepth()
        CARD_DB.load()
        if len(CARD_DB) == 0:
            return self.fatal_error(self, text=_('No cardsets were found.'),
                                    err=3)

        pack_name = CONF_OPT['cardset'].get('0')[0]
        if pack_name in CARD_DB:
            CARD_DB.choice(CARD_DB.find(pack_name)['index'])
        else:
            CARD_DB.choice(0)

        if self.progress:
            self.progress.update(step=1)

    def _init_tiledb(self, args):
        """
        int_tiledb(args)

        Initialize tile db and load independent tile modules. Sets tile to
        matching option.

        :requires:
          TILE_DB
        """
        from . import resourceng as r
        # Set first dummy tile
        tile = r.Tile(color=CONF_OPT['colors']['table'], name='None',
                          filename=None)
        r.TILE_DB.register(tile)
        r.TILE_DB.choice(0)
        # Find and load available tiles by file extension
        s = '((\\' + ')|(\\'.join(settings.IMAGE_EXTENSIONS) + '))$'
        r.TILE_DB.load(self, dirs=('tiles-*', 'tiles',
                                 os.path.join('tiles', 'stretch'),
                                 os.path.join('tiles', 'save-aspect')),
                                 env='PYSOL_TILES', ext=s, src=r.Tile)
        if self.progress:
            self.progress.update(step=1)
        if CONF_OPT.get('tabletile_name') is not None:
            for tile in r.TILE_DB:
                if CONF_OPT.get('tabletile_name') == tile['basename']:
                    r.TILE_DB.choice(tile['index'])
                    break
        if self.progress:
            self.progress.update(step=1)

    def _load_held_game(self):
        """
        _loadHeldGame() -> None

        Load a held game id from option 'game_holded'.

        :IVariables:
          nextgame : Map(id, random, loadedgame, startdemo, holdgame, bookmark)
            set loadedgame value to 'game_holded' option.
        """
        holdgame = PATH_DB.fn['holdgame']
        if any([not CONF_OPT['game_holded'], self.nextgame['data'] is not None,
                not os.path.exists(holdgame)]):
            return
        tmpgame = self.construct_game()
        self.top.wm_set_cursor(cursor=CURSOR_WATCH)
        game = tmpgame._load_data(filename=holdgame)
        self.top.wm_set_cursor()
        if game:
            counter = self.nextgame['counter']
            if game['id'] == CONF_OPT['game_holded'] and counter['held']:
                counter['load'] -= 1
                counter['held'] -= 1
                self.nextgame['data'] = game
            else:
#                game.destruct()
                del(game)
#        tmpgame.destruct()
        del(tmpgame)

    def _load_save_game(self, load):
        """
        _load_save_game(load) -> None

        Load a saved game.

        :Keywords:
          load : str
            file name for saved game.

          name : str
            Game name to play next.

          gid : int
            Game id to play next.

        :IVariables:
          nextgame : Map(id, random, loadedgame, startdemo, holdgame, bookmark)
            set loadedgame value.
        """
        if any([not load, load and not os.path.exists(load),
                self.nextgame['data'] is not None]):
            return
        tmpgame = self.construct_game()
        self.top.wm_set_cursor(cursor=CURSOR_WATCH)
        self.nextgame['data'] = tmpgame._load_data(filename=load)
        self.top.wm_set_cursor()
#        tmpgame.destruct()
        del(tmpgame)

    def _load_canvas(self):
        """
        _load_canvas() -> None

        Load a saved game or from name then id from command line option.

        :IVariables:
          menubar : PysolMenubar
            initialize app menus.

          statusbar : PysolStatusbar
            initialize app status area.

          helpbar : HelpStatusbar
            initalize app help area.

          canvas : ScrolledCanvas
            initialize app playing canvas area.

          toolbar : PysolToolbar
            initialize app icon area.
        """
        #from winsystems import TkSettings
        from .viewlib.toolkit import Canvas
        from .viewlib.console import MENU_BAR, STATUS_BAR, ASSIST_BAR, TOOL_BAR
        # create the menubar
        if self.progress:
            self.progress.update(step=1)
        toplevel = self.top
        MENU_BAR.connect_app(self, progress=self.progress)
        # create the statusbar(s)
        STATUS_BAR.connect_top(toplevel)
        ASSIST_BAR.connect_top(toplevel)
        # create the canvas
        self.canvas = Canvas(toplevel)
        self.set_background(force=True)
        # create the toolbar
        TOOL_BAR.connect_top(toplevel)

    def mainloop(self):
        """
        mainloop() -> None

        Run the game mainloop with values from nextgame. Save game when
        nextgame['holded'] and update 'game_holded' option. Update the
        'games_geometry' option when 'save_games_geometry'] and not
        'wm_maximized' option. When nextgame['id'] is None, save 'last_gameid'
        option and break loop.
        """
        while True:
            id_, random = self._locate()
            try:
                self._connect(id_)
                if self.progress:
                    self.progress.destroy()
#                    destruct(self.progress)
                    self.progress = None
                self._run_game(random)
            except KeyboardInterrupt:
                break
            #except Exception as err:
            #    Logger.error(err)
            nextgame = self.nextgame
            if nextgame['mode'] == 'hold':
                nextgame['id'] = 0
                nextgame['counter']['held'] += 1
                self.top.wm_set_cursor(cursor=CURSOR_WATCH)
                self.game._dump_data(filename=PATH_DB.fn['holdgame'])
                self.top.wm_set_cursor()

            if nextgame['mode'] == 'mark':
                nextgame['id'] = self.game['id']
                nextgame['random'] = self.game.random
                self.top.wm_set_cursor(cursor=CURSOR_WATCH)
                self._dump_data(bookmark=0, update_stats=1)
                self.top.wm_set_cursor()

            if nextgame['mode'] == 'load' and nextgame['data'] is not None:
                nextgame['id'] = nextgame['data']['id']
                nextgame['random'] = nextgame['data'].random

            # save game geometry
            if (CONF_OPT['save_games_geometry'] and
                not CONF_OPT['wm_maximized']):
                geom = (self.canvas.winfo_width(), self.canvas.winfo_height())
                CONF_OPT['games_geometry'][self.game['id']] = geom

            self._disconnect()

            if nextgame['id'] > 0:
                self.top.wm_set_cursor(cursor=CURSOR_WATCH)
            else:
                CONF_OPT['last_gameid'] = id_
                break

    def _locate(self):
        """
        _locate() -> id_, random

        Set id and seed for next_game. Attempt to locate in GAME_DB, then try
        default game, then try 1st game.

        :IVariables:
          nextgame : Map(id, random, loadedgame, startdemo, holdgame, bookmark)
            Unset id and random values after locate.

        :returns: located gameid, and random when nextgame succeeds else None
            from initial nextgame.
        """
        from .gamelib.data import GAME_DB
        id_, random = self.nextgame['id'], self.nextgame['random']
        Logger.info('Initial game id %d choice.', id_)
        gid = (id_ or GAME_DB[GAME_DB.find(2, 'id')]['id'] or
               GAME_DB.sort('name')[0])
        gi = GAME_DB[GAME_DB.find(gid, 'id')]
        assert gi is not None
        self.nextgame.update({'id': 0, 'random': None})
        Logger.info('Final game id %d choice.', gid)
        return gid, random if gid == id_ else None

    def construct_game(self, id_=None):
        """
        construct_game(id_=None) -> Game

        Initialize game object from GAME_DB of id_.

        :Parameters:
          id_ : int
            game id of game in GAME_DB.
        """
        from .gamelib.data import GAME_DB
        gid = id_ or GAME_DB.sort('name')[0]
        return GAME_DB[GAME_DB.find(gid, 'id')]()

    def _connect(self, id_):
        """
        _connect(id_) -> None

        Connect game object to Canvas, Menu, ToolBar and Dialog.

        :Parameters:
          id_ : int
            game id of game in GAME_DB.

        :IVariables:
          game : Game
            construct a game with GAME_DB id_.

        :note:
          This sets game.busy True which is set then False by _run_game().
        """
        from .viewlib.console import MENU_BAR, TOOL_BAR
        from .gamelib.data import GAME_DB
        self.game = game = self.construct_game(id_)
        self.top.connect_game(game)
        GAME_DB.choice(GAME_DB.find(id_, 'id'))
        # create stacks and layout
        game.prepare(self)
        # connect with game
        MENU_BAR.connect_game(game)
        Dialog.connect_game(game)
        TOOL_BAR.connect_game(game)

    def _run_game(self, game, random=None):
        """
        _run_game(id_, random=None) -> id_, random

        Run game object by restoring `nextgame['loadedgame']` or
        `nextgame['bookmark']` or `new` with random seed.

        :Parameters:
          game : Game
            game id of game in GAME_DB.

        :Keywords:
          random : int
            seed value.

        :IVariables:
          nextgame : Map(id, random, loadedgame, startdemo, holdgame, bookmark)
            Unset loadedgame and bookmark after restore.

            When 'splashscreen' option set startdemo True when not loadedgame
            or holdgame.

          splashscreen : bool
            Set False.

        :note:
          Initial game will display a splash screen when 'splashscreen' option
          and if not dismissed will start demo when not a loadedgame or
          bookmark.

          This sets game.busy False which was set True by _connect().
        """
        nextgame = self.nextgame
        # splash screen
        status = True
        if CONF_OPT['splashscreen'] and self.splashscreen:
            status = Dialog.about(timeout=20000)
            # timeout starts a demo
        if nextgame['mode'] == 'load' and nextgame['data'] is not None:
            self.game.restore(nextgame['data'])
            nextgame['counter']['load'] += 1
        elif nextgame['mode'] == 'mark' and nextgame['bookmark'][0] is not None:
            self.top.wm_set_cursor(cursor=CURSOR_WATCH)
            self.game.restore(self.game._load_data(bookmark=0), reset=False)
            self.top.wm_set_cursor()
        else:
            self.game.new(random=random)

        if nextgame['mode'] == 'demo' or not status:
            self.game.demo_start(mixed=True)

        self.splashscreen = False
        nextgame['mode'] = nextgame['bookmark'][0] = nextgame['data'] = None
        self.top.mainloop()

    def _disconnect(self):
        """
        _disconnect() -> None

        Disconnect game object from Canvas, Menu, ToolBar and Dialog.

        :Parameters:
          id_ : int
            game id of game in GAME_DB.

        :IVariables:
          game : Game
            construct a game with GAME_DB id_.

        :note:
          This sets game.busy True which is set then False by _run_game().
        """
        # clean up the canvas
        self.canvas.reset()
        self.canvas.update_idletasks()
        self.game = None
        self.top.connect_game(None)

    # UI support
#    def wm_save_state(self):
#        """
#        wm_save_state() -> None

#        Save current window state to 'wm_maximized' option.
#        """
#        if self.top:
#            s = self.top.wm_state()
#            if s == 'zoomed':  # Windows only
#                CONF_OPT['wm_maximized'] = True
#            elif s == 'normal':
#                CONF_OPT['wm_maximized'] = False

    def wm_withdraw(self):
        """
        wm_withdraw() -> None

        Delete current progress dialog and / or canvas.
        """
        if self.progress:
            self.progress.destroy()
            self.progress = None
        if self.top:
            self.top.wm_withdraw()
            self.top.update()

    def set_background(self, force=False):
        """
        set_background(force=False) -> bool

        Set canvas background to TILE_DB.choice() and update options.

        :Keywords:
          force : bool
            change regardless.

        :returns: success changing background.
        """
        tile_id = TILE_DB.choice()
        if self.canvas.set_background(tile_id, force=force):
            tile = TILE_DB[tile_id]
            if tile_id == 0:
                CONF_OPT['colors']['table'] = tile['color']
                CONF_OPT['tabletile_name'] = None
            else:
                CONF_OPT['tabletile_name'] = tile['basename']
            self.canvas.set_textcolor(CONF_OPT['colors']['text'])
            return True
        return False

    def fatal_error(self, text, err):
        """
        fatal_message(text, err) -> None

        Show a fatal message dialog of text and exit with passed err.
        """
        self.wm_withdraw()
        Dialog.error(text=text + _('Main data directory is:\n%s\nPlease '
                     'check your installation.') % (PATH_DB.dn['data']))
        sys.exit(err)

    def __exit__(self, err_type, err_value, traceback):
        """
        __exit__(err_type, err_value, traceback) -> bool

        Destroy canvas and elements then save game data final destroy audio.
        """
        self.wm_withdraw()

        CONF_OPT.write()  # save options
        from .statdb import STAT_DB
        STAT_DB.save(PATH_DB.fn['stats'])  # save statistics
        NOTE_DB.save(PATH_DB.fn['notes'])  # save comments
        AUDIO_DEV.destroy()  # shut down audio
        return False